The Pizza Problem

Adam Shen

February 17, 2026

I've got 99 problems and dinner is one

Let's set the scene:

  1. I've just finished work. I'm exhausted 😵 and I don't want to prepare my own dinner.

  2. Takeout or pick-up over dine-in or delivery because I don't want to be paying extra tips or service charges 😒.

  3. I've been eating a lot of 🐔 chicken and 🥬 leafy salads 🥗 lately, and I want something with a different flavour profile.

  4. 💪🏼 I'm trying to hit my daily protein goals while also making sure that I'm not going too overboard with the calories (a little over is okay).

  5. Skipping meals is 🙅🏼‍♂️ when you're trying to make 💪🏼 gainsss 🦵🏼.

  6. The food needs to be ready reasonably fast so that I have enough time to digest it before heading to the gym 🏋🏼‍♂️ later in the evening.

Solution: Little Caesars

(Not sponsored btw)

Why?

  1. Their sauce is 👌🏼 perfectly 👌🏼 seasoned.

  2. Their dough is also perfect: crispy on the outside and soft and airy on the inside. (Unlike Domino's where the crust is super thick and dense 😒)

  3. Their items are reasonably priced 🫰🏼 for the amount of food you get and there are often discounts on the app or on social media 🤑.

  4. Leftovers reheat well so you can eat it again tomorrow 🤤. See this tip (Instagram).

FYI: These deals run until April 19, 2026

 

Fine print: Max. 10 per transaction!

Now I've got new problems

  1. Of their "core" pizza offerings, which pizza is the 👍🏼💰 most cost effective while having the ⬇️🫃🏼 least calories and the ⬆️💪🏼 most protein? (We're going to ignore fat, cholesterol, sodium, etc.)

  2. Does this change under the conditions of either of these deals:

    • "Large pepperoni/cheese pizza for $9.99".
    • "Large specialty pizza for $13.99".

Tip

Recall that (in most cases) a food is considered "high-protein" if its protein content (in grams) is at least one-tenth of its calories. For example, a food with 100 calories should have at least 10g of protein.

The gold star reference

As of February 2026, the price of Fairlife Nutrition Plan shakes has increased to $49.99 for a case of 18 at Costco.

Each bottle has 150 calories and 30g of protein (high protein).

This works out to be $2.78 per bottle, or $0.09267 per gram of protein. Calorie-to-protein ratio is 5 (lower is better).

Metrics

  1. Price per gram of protein.
    • Fairlife: $0.09267 per gram of protein.
  2. Calorie-to-protein ratio.
    • Fairlife: 5.

The lcpizza package

To aid me in this analysis, I recently took the time to collect the nutrition and local pricing data and bundle it up as the R package, lcpizza!

The package can be installed as usual using either:

# install.packages("pak")
pak::pak("adamoshen/lcpizza")

Or,

# install.packages("remotes")
remotes::install_github("adamoshen/lcpizza")

The analysis

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
 dplyr     1.2.0      readr     2.1.6
 forcats   1.0.1      stringr   1.6.0
 ggplot2   4.0.2      tibble    3.3.1
 lubridate 1.9.5      tidyr     1.3.2
 purrr     1.2.1     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
 dplyr::filter() masks stats::filter()
 dplyr::lag()    masks stats::lag()
 Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(patchwork)
library(lcpizza)

Intro to lcpizza

pizzas
# A tibble: 27 × 18
   size   crust   flavour price calories total_fat sat_fat trans_fat cholesterol
   <chr>  <chr>   <chr>   <dbl>    <dbl>     <dbl>   <dbl>     <dbl>       <dbl>
 1 medium regular Pepper…  7.99     1590        63      29       1.5         125
 2 medium regular Cheese   7.99     1400        46      22       0.5          85
 3 medium regular Ultima… 11.5      1750        74      33       1.5         150
 4 medium regular 3 Meat… 11.5      1980        98      41       1.5         205
 5 medium regular Canadi… 11.5      1820        84      36       1.5         175
 6 medium regular Hula H… 11.5      1590        49      23       0.5         125
 7 medium regular Veggie  12.5      1520        53      23       0.5          85
 8 medium regular BBQ Ch… 12.5      1550        48      23       0.5         130
 9 medium stuffed Cheese  12.5      1760        75      39       1.5         165
10 medium stuffed Pepper… 12.5      1960        93      46       2.5         205
# ℹ 17 more rows
# ℹ 9 more variables: sodium <dbl>, total_carb <dbl>, fibre <dbl>, sugar <dbl>,
#   protein <dbl>, vit_a <dbl>, vit_c <dbl>, calcium <dbl>, iron <dbl>

Intro to lcpizza

toppings
# A tibble: 32 × 17
   size   topping  price calories total_fat sat_fat trans_fat cholesterol sodium
   <chr>  <chr>    <dbl>    <dbl>     <dbl>   <dbl>     <dbl>       <dbl>  <dbl>
 1 medium peppero…   2        260      23         9         1          55   1140
 2 medium pineapp…   2         80       0         0         0           0      0
 3 medium ham        2         80       2.5       1         0          35    910
 4 medium mushroo…   2         10       0         0         0           0      0
 5 medium bacon      2        280      26         9         0          65    990
 6 medium green p…   2         25       0         0         0           0      0
 7 medium onions     2         25       0         0         0           0      0
 8 medium sausage    2        230      20         7         0          40    720
 9 medium chicken    2.5       80       1.5       0         0          40    490
10 medium red oni…   2         25       0         0         0           0      0
# ℹ 22 more rows
# ℹ 8 more variables: total_carb <dbl>, fibre <dbl>, sugar <dbl>,
#   protein <dbl>, vit_a <dbl>, vit_c <dbl>, calcium <dbl>, iron <dbl>

Assemble our reference data

Code
fairlife <- tibble(
  name = "Fairlife Protein Shake",
  price = 49.99 / 18,
  calories = 150,
  protein = 30
)
fairlife
# A tibble: 1 × 4
  name                   price calories protein
  <chr>                  <dbl>    <dbl>   <dbl>
1 Fairlife Protein Shake  2.78      150      30

Write function to get our metrics

Code
bind_metrics <- function(.data) {
  mutate(
    .data,
    price_per_g_protein = price / protein,
    calorie_protein_ratio = calories / protein
  )
}
Code
bind_metrics(fairlife)
# A tibble: 1 × 6
  name          price calories protein price_per_g_protein calorie_protein_ratio
  <chr>         <dbl>    <dbl>   <dbl>               <dbl>                 <dbl>
1 Fairlife Pro…  2.78      150      30              0.0926                     5

The basics

Let's first focus on the medium cheese, medium pepperoni, large cheese, and large pepperoni pizzas.

Code
basic_pizzas <- pizzas %>%
  filter(
    size %in% c("medium", "large"),
    crust == "regular",
    flavour %in% c("Cheese", "Pepperoni")
  )
Code
basic_pizzas %>%
  select(size, flavour, price, calories, protein)
# A tibble: 4 × 5
  size   flavour   price calories protein
  <chr>  <chr>     <dbl>    <dbl>   <dbl>
1 medium Pepperoni  7.99     1590      75
2 medium Cheese     7.99     1400      66
3 large  Pepperoni 13.5      2160     105
4 large  Cheese    13.5      1920      94

Comparing Fairlife with the basic pizzas

Code
basic_comparison <- basic_pizzas %>%
  mutate(size = str_to_title(size)) %>%
  unite(name, size, flavour, sep = " ") %>%
  bind_rows(fairlife) %>%
  bind_metrics() %>%
  select(name, price, calories, protein, price_per_g_protein, calorie_protein_ratio)
Code
basic_comparison
# A tibble: 5 × 6
  name          price calories protein price_per_g_protein calorie_protein_ratio
  <chr>         <dbl>    <dbl>   <dbl>               <dbl>                 <dbl>
1 Medium Peppe…  7.99     1590      75              0.107                   21.2
2 Medium Cheese  7.99     1400      66              0.121                   21.2
3 Large Pepper… 13.5      2160     105              0.128                   20.6
4 Large Cheese  13.5      1920      94              0.144                   20.4
5 Fairlife Pro…  2.78      150      30              0.0926                   5  

Comparing Fairlife with basic pizzas with discount

Code
basic_discounts <- tibble(
  name = c("Large Pepperoni", "Large Cheese"),
  price = 9.99
)

basic_comparison_with_discounts <- basic_pizzas %>%
  mutate(size = str_to_title(size)) %>%
  unite(name, size, flavour, sep = " ") %>%
  bind_rows(fairlife) %>%
  rows_update(basic_discounts, by = "name") %>%
  bind_metrics() %>%
  select(name, price, calories, protein, price_per_g_protein, calorie_protein_ratio)

Effect of discount on basic pizzas

Without discount:

# A tibble: 5 × 6
  name          price calories protein price_per_g_protein calorie_protein_ratio
  <chr>         <dbl>    <dbl>   <dbl>               <dbl>                 <dbl>
1 Medium Peppe…  7.99     1590      75              0.107                   21.2
2 Medium Cheese  7.99     1400      66              0.121                   21.2
3 Large Pepper… 13.5      2160     105              0.128                   20.6
4 Large Cheese  13.5      1920      94              0.144                   20.4
5 Fairlife Pro…  2.78      150      30              0.0926                   5  

With discount:

# A tibble: 5 × 6
  name          price calories protein price_per_g_protein calorie_protein_ratio
  <chr>         <dbl>    <dbl>   <dbl>               <dbl>                 <dbl>
1 Medium Peppe…  7.99     1590      75              0.107                   21.2
2 Medium Cheese  7.99     1400      66              0.121                   21.2
3 Large Pepper…  9.99     2160     105              0.0951                  20.6
4 Large Cheese   9.99     1920      94              0.106                   20.4
5 Fairlife Pro…  2.78      150      30              0.0926                   5  

The discount makes the Large Pepperoni pizza comparable to the Fairlife Shake in Price per Grams of Protein 🤯!

Specialty pizzas

Code
specialty_pizzas <- pizzas %>%
  filter(crust == "regular") %>%
  filter_out(flavour %in% c("Cheese", "Pepperoni")) # Requires dplyr 1.2.0
Code
specialty_pizzas %>%
  select(size, flavour, price, calories, protein)
# A tibble: 12 × 5
   size   flavour          price calories protein
   <chr>  <chr>            <dbl>    <dbl>   <dbl>
 1 medium Ultimate Supreme  11.5     1750      83
 2 medium 3 Meat Treat      11.5     1980      90
 3 medium Canadian          11.5     1820      83
 4 medium Hula Hawaiian     11.5     1590      83
 5 medium Veggie            12.5     1520      70
 6 medium BBQ Chicken       12.5     1550      84
 7 large  Ultimate Supreme  16.5     2390     115
 8 large  3 Meat Treat      16.5     2560     119
 9 large  Hula Hawaiian     16.5     2160     115
10 large  Canadian          17.0     2460     115
11 large  Veggie            17.5     2070     100
12 large  BBQ Chicken       18.5     2110     120

Comparing Fairlife with the specialty pizzas

specialty_comparison <- specialty_pizzas %>%
  mutate(size = str_to_title(size)) %>%
  unite(name, size, flavour, sep = " ") %>%
  bind_rows(fairlife) %>%
  bind_metrics() %>%
  select(name, price, calories, protein, price_per_g_protein, calorie_protein_ratio)
# A tibble: 13 × 6
   name         price calories protein price_per_g_protein calorie_protein_ratio
   <chr>        <dbl>    <dbl>   <dbl>               <dbl>                 <dbl>
 1 Medium Ulti… 11.5      1750      83              0.138                   21.1
 2 Medium 3 Me… 11.5      1980      90              0.128                   22  
 3 Medium Cana… 11.5      1820      83              0.138                   21.9
 4 Medium Hula… 11.5      1590      83              0.138                   19.2
 5 Medium Vegg… 12.5      1520      70              0.178                   21.7
 6 Medium BBQ … 12.5      1550      84              0.149                   18.5
 7 Large Ultim… 16.5      2390     115              0.143                   20.8
 8 Large 3 Mea… 16.5      2560     119              0.139                   21.5
 9 Large Hula … 16.5      2160     115              0.143                   18.8
10 Large Canad… 17.0      2460     115              0.148                   21.4
11 Large Veggie 17.5      2070     100              0.175                   20.7
12 Large BBQ C… 18.5      2110     120              0.154                   17.6
13 Fairlife Pr…  2.78      150      30              0.0926                   5  

Effect of discount on specialty pizzas

specialty_discounts <- tibble(
  name = c("Large 3 Meat Treat", "Large Canadian", "Large Ultimate Supreme", "Large Hula Hawaiian"),
  price = 13.99
)

specialty_comparison_with_discounts <- specialty_pizzas %>%
  mutate(size = str_to_title(size)) %>%
  unite(name, size, flavour, sep = " ") %>%
  bind_rows(fairlife) %>%
  rows_update(specialty_discounts, by = "name") %>%
  bind_metrics() %>%
  select(name, price, calories, protein, price_per_g_protein, calorie_protein_ratio)

Note: The Large Veggie and Large BBQ Chicken are excluded from these discounts!

Without discounts, the Medium specialty pizzas actually have a lower Price Per Gram of Protein! Unsurprisingly, the Hula Hawaiian and the BBQ Chicken have the lowest Calorie to Protein Ratios (after the Fairlife Shake). I'm not really a fan of the BBQ Chicken pizza though 🫤.

With discounts, the Large specialty pizzas have a better Price Per Gram of Protein than their Medium counterparts and get closer to the Fairlife Shake. The Large Hawaiian pizza excels over both metrics!

Visual summaries

Large pizzas without discount

large_pizzas <- pizzas %>%
  filter(size == "large", crust == "regular") %>%
  mutate(size = str_to_title(size)) %>%
  unite(name, size, flavour, sep = " ") %>%
  bind_rows(fairlife) %>%
  bind_metrics() %>%
  select(name, price, calories, protein, price_per_g_protein, calorie_protein_ratio) %>%
  mutate(bar_fill = if_else(str_detect(name, "Fairlife"), "#9C755F", "#F28E2B"))
large_ppgp <- large_pizzas %>%
  slice_min(price_per_g_protein, n = 7) %>%
  mutate(
    name = fct_reorder(name, price_per_g_protein),
    price_label = scales::number(price_per_g_protein, accuracy = 0.0001, prefix = "$")
  ) %>%
  ggplot(aes(x = price_per_g_protein, y = name)) +
  geom_col(aes(fill = bar_fill), show.legend = FALSE) +
  geom_text(aes(label = price_label), nudge_x = 0.03) +
  scale_fill_identity() +
  theme_minimal() +
  theme_sub_axis_x(text = element_blank()) +
  theme_sub_axis_y(text = element_text(size = 12)) +
  theme_sub_panel(grid.major = element_blank(), grid.minor = element_blank()) +
  coord_cartesian(clip = "off") +
  labs(y = "", x = "Price per gram of protein (lower is better)")

large_cpr <- large_pizzas %>%
  slice_min(calorie_protein_ratio, n = 7) %>%
  mutate(
    name = fct_reorder(name, calorie_protein_ratio),
    ratio_label = scales::number(calorie_protein_ratio, accuracy = 0.01)
  ) %>%
  ggplot(aes(x = calorie_protein_ratio, y = name)) +
  geom_col(aes(fill = bar_fill), show.legend = FALSE) +
  geom_text(aes(label = ratio_label), nudge_x = 3) +
  scale_fill_identity() +
  theme_minimal() +
  theme_sub_axis_x(text = element_blank()) +
  theme_sub_axis_y(text = element_text(size = 12)) +
  theme_sub_panel(grid.major = element_blank(), grid.minor = element_blank()) +
  coord_cartesian(clip = "off") +
  labs(y = "", x = "Calorie to protein ratio (lower is better)")

large_ppgp + large_cpr

Large pizzas with discount

large_pizzas_with_discount <- pizzas %>%
  filter(size == "large", crust == "regular") %>%
  mutate(size = str_to_title(size)) %>%
  unite(name, size, flavour, sep = " ") %>%
  rows_update(basic_discounts, by = "name") %>%
  rows_update(specialty_discounts, by = "name") %>%
  bind_rows(fairlife) %>%
  bind_metrics() %>%
  select(name, price, calories, protein, price_per_g_protein, calorie_protein_ratio) %>%
  mutate(bar_fill = if_else(str_detect(name, "Fairlife"), "#9C755F", "#F28E2B"))
large_ppgp_discount <- large_pizzas_with_discount %>%
  slice_min(price_per_g_protein, n = 7) %>%
  mutate(
    name = fct_reorder(name, price_per_g_protein),
    price_label = scales::number(price_per_g_protein, accuracy = 0.0001, prefix = "$")
  ) %>%
  ggplot(aes(x = price_per_g_protein, y = name)) +
  geom_col(aes(fill = bar_fill), show.legend = FALSE) +
  geom_text(aes(label = price_label), nudge_x = 0.03) +
  scale_fill_identity() +
  theme_minimal() +
  theme_sub_axis_x(text = element_blank()) +
  theme_sub_axis_y(text = element_text(size = 12)) +
  theme_sub_panel(grid.major = element_blank(), grid.minor = element_blank()) +
  coord_cartesian(clip = "off") +
  labs(y = "", x = "Price per gram of protein (lower is better)")

large_cpr_discount <- large_pizzas_with_discount %>%
  slice_min(calorie_protein_ratio, n = 7) %>%
  mutate(
    name = fct_reorder(name, calorie_protein_ratio),
    ratio_label = scales::number(calorie_protein_ratio, accuracy = 0.01)
  ) %>%
  ggplot(aes(x = calorie_protein_ratio, y = name)) +
  geom_col(aes(fill = bar_fill), show.legend = FALSE) +
  geom_text(aes(label = ratio_label), nudge_x = 3) +
  scale_fill_identity() +
  theme_minimal() +
  theme_sub_axis_x(text = element_blank()) +
  theme_sub_axis_y(text = element_text(size = 12)) +
  theme_sub_panel(grid.major = element_blank(), grid.minor = element_blank()) +
  coord_cartesian(clip = "off") +
  labs(y = "", x = "Calorie to protein ratio (lower is better)")

large_ppgp_discount + large_cpr_discount

Afterthought

Rather than looking at price per gram of protein, it probably would have been better to look at the price per 30g of protein to get an idea of how much you would be paying for pizza compared to a bottle of the Fairlife protein shake.

bind_metrics2 <- function(.data) {
  mutate(
    .data,
    price_per_30g_protein = price / protein * 30,
    calorie_protein_ratio = calories / protein
  )
}

Large pizzas without discount

large_pizzas2 <- pizzas %>%
  filter(size == "large", crust == "regular") %>%
  mutate(size = str_to_title(size)) %>%
  unite(name, size, flavour, sep = " ") %>%
  bind_rows(fairlife) %>%
  bind_metrics2() %>%
  select(name, price, calories, protein, price_per_30g_protein, calorie_protein_ratio) %>%
  mutate(bar_fill = if_else(str_detect(name, "Fairlife"), "#9C755F", "#F28E2B"))
large_ppgp2 <- large_pizzas2 %>%
  slice_min(price_per_30g_protein, n = 7) %>%
  mutate(
    name = fct_reorder(name, price_per_30g_protein),
    price_label = scales::number(price_per_30g_protein, accuracy = 0.01, prefix = "$")
  ) %>%
  ggplot(aes(x = price_per_30g_protein, y = name)) +
  geom_col(aes(fill = bar_fill), show.legend = FALSE) +
  geom_text(aes(label = price_label), nudge_x = 0.60) +
  scale_fill_identity() +
  theme_minimal() +
  theme_sub_axis_x(text = element_blank()) +
  theme_sub_axis_y(text = element_text(size = 12)) +
  theme_sub_panel(grid.major = element_blank(), grid.minor = element_blank()) +
  coord_cartesian(clip = "off") +
  labs(y = "", x = "Price per 30 grams of protein (lower is better)")
  

large_cpr2 <- large_pizzas2 %>%
  slice_min(calorie_protein_ratio, n = 7) %>%
  mutate(
    name = fct_reorder(name, calorie_protein_ratio),
    ratio_label = scales::number(calorie_protein_ratio, accuracy = 0.01)
  ) %>%
  ggplot(aes(x = calorie_protein_ratio, y = name)) +
  geom_col(aes(fill = bar_fill), show.legend = FALSE) +
  geom_text(aes(label = ratio_label), nudge_x = 3) +
  scale_fill_identity() +
  theme_minimal() +
  theme_sub_axis_x(text = element_blank()) +
  theme_sub_axis_y(text = element_text(size = 12)) +
  theme_sub_panel(grid.major = element_blank(), grid.minor = element_blank()) +
  coord_cartesian(clip = "off") +
  labs(y = "", x = "Calorie to protein ratio (lower is better)")
  
large_ppgp2 + large_cpr2

Large pizzas with discount

large_pizzas2_with_discount <- pizzas %>%
  filter(size == "large", crust == "regular") %>%
  mutate(size = str_to_title(size)) %>%
  unite(name, size, flavour, sep = " ") %>%
  rows_update(basic_discounts, by = "name") %>%
  rows_update(specialty_discounts, by = "name") %>%
  bind_rows(fairlife) %>%
  bind_metrics2() %>%
  select(name, price, calories, protein, price_per_30g_protein, calorie_protein_ratio) %>%
  mutate(bar_fill = if_else(str_detect(name, "Fairlife"), "#9C755F", "#F28E2B"))
large_ppgp_discount2 <- large_pizzas2_with_discount %>%
  slice_min(price_per_30g_protein, n = 7) %>%
  mutate(
    name = fct_reorder(name, price_per_30g_protein),
    price_label = scales::number(price_per_30g_protein, accuracy = 0.01, prefix = "$")
  ) %>%
  ggplot(aes(x = price_per_30g_protein, y = name)) +
  geom_col(aes(fill = bar_fill), show.legend = FALSE) +
  geom_text(aes(label = price_label), nudge_x = 0.60) +
  scale_fill_identity() +
  theme_minimal() +
  theme_sub_axis_x(text = element_blank()) +
  theme_sub_axis_y(text = element_text(size = 12)) +
  theme_sub_panel(grid.major = element_blank(), grid.minor = element_blank()) +
  coord_cartesian(clip = "off") +
  labs(y = "", x = "Price per 30 grams of protein (lower is better)")

large_cpr_discount2 <- large_pizzas2_with_discount %>%
  slice_min(calorie_protein_ratio, n = 7) %>%
  mutate(
    name = fct_reorder(name, calorie_protein_ratio),
    ratio_label = scales::number(calorie_protein_ratio, accuracy = 0.01)
  ) %>%
  ggplot(aes(x = calorie_protein_ratio, y = name)) +
  geom_col(aes(fill = bar_fill), show.legend = FALSE) +
  geom_text(aes(label = ratio_label), nudge_x = 3) +
  scale_fill_identity() +
  theme_minimal() +
  theme_sub_axis_x(text = element_blank()) +
  theme_sub_axis_y(text = element_text(size = 12)) +
  theme_sub_panel(grid.major = element_blank(), grid.minor = element_blank()) +
  coord_cartesian(clip = "off") +
  labs(y = "", x = "Calorie to protein ratio (lower is better)")

large_ppgp_discount2 + large_cpr_discount2

Summary

  • With or without discounts, the Large BBQ Chicken and Large Hula Hawaiian pizzas are slightly more "health conscious" 😏.

  • With discounts, the Large Pepperoni is comparable to the Fairlife Protein Shake in price per gram of protein. However, the Large Hawaiian is not too far off!

  • Therefore, if you are trying to make gains, save money, and let loose once in a while, the Large Hawaiian pizza is the way to go.

Alternative conclusion: 🍍 Pineapple 🍍 belongs on pizza 😎